<template>
  <div class="works-container">
    <div class="works-aside">
      <template v-for="userId in userIds" :key="userId">
        <WorksAvatar
          v-if="userId in worksState"
          :avatar="worksState[userId].avatar"
          :name="worksState[userId].name || userId"
          :active="globalState.userId === userId"
          @click="onAvatarClick(userId)"
          @remove="onUserIdRemove(userId)"
        />
      </template>
    </div>
    <div class="works-content">
      <div class="works-content__header">
        <el-checkbox
          :indeterminate="globalState.indeterminate"
          :model-value="globalState.checkAll"
          @change="onCheckAllChange"
        >
          全选
        </el-checkbox>
        <el-icon class="cursor-pointer" title="下载已勾选" :size="18" @click="onCheckedDownload"><Download /></el-icon>
        <el-icon
          class="cursor-pointer"
          :size="18"
          @click="globalState.sort = globalState.sort === SortType.Down ? SortType.Up : SortType.Down"
        >
          <SortAmountDown v-if="globalState.sort === SortType.Down" />
          <SortAmountUp v-else />
        </el-icon>
        <el-input v-model.trim="globalState.filter" style="width: 240px" placeholder="过滤" />
        <el-popover title="配置" :width="450" trigger="click">
          <template #reference>
            <el-icon class="cursor-pointer align-right" :size="18"><Tools /></el-icon>
          </template>
          <el-form label-width="auto">
            <el-form-item label="文件保存根目录">
              <el-button class="works-directory-button" @click="onRootDirectoryPick">
                <el-icon :size="18"><RootDirectory /></el-icon>：{{ rootHandle?.name }}
              </el-button>
            </el-form-item>
            <el-form-item label="当前作品集保存目录">
              <el-button class="works-directory-button" :disabled="!globalState.userId" @click="onWorksDirectoryPick">
                <el-icon :size="18"><WorksDirectory /></el-icon>：{{ worksHandle?.name }}
              </el-button>
            </el-form-item>
            <el-form-item label="最大下载数">
              <el-select v-model="downloadState.max">
                <el-option :value="1" />
                <el-option :value="2" />
                <el-option :value="3" />
                <el-option :value="4" />
                <el-option :value="5" />
              </el-select>
            </el-form-item>
          </el-form>
        </el-popover>
      </div>
      <el-checkbox-group
        class="works-content__body"
        :model-value="globalState.checked"
        @update:model-value="onUpdateChecked"
      >
        <VirtualList
          key="id"
          style="padding: 0 20px"
          :data-source="activeWorksList"
          :layout="{ itemWidth: 200, itemHeight: 320, gap: 20, overscan: 1 }"
        >
          <template #default="{ data, width, height }">
            <div class="works-card-container">
              <div class="works-card-tool">
                <el-checkbox class="works-checkbox" :value="(data as Works).id" />
                <el-icon :size="20" color="#fff" @click="onDownload(data as Works)"><Download /></el-icon>
              </div>
              <WorksDetails :works="(data as Works)" :width="width" :height="height" />
            </div>
          </template>
        </VirtualList>
      </el-checkbox-group>
    </div>
    <WorksDownload
      :data-source="downloadState.awaitList"
      @remove="onAwaitDownloadRemove"
      @remove-all="onAwaitDownloadRemove"
      @stop="onAwaitDownloadStop"
      @stop-all="onAwaitDownloadStop"
      @download="onAwaitDownload"
      @download-all="onAwaitDownloadAll"
    />
  </div>
</template>

<script lang="ts">
import { computedAsync, useStorage } from '@vueuse/core'
import { Download, Folder, FolderOpened, Tools } from '@element-plus/icons-vue'
import { storage, type Unwatch } from 'wxt/storage'
import { ElMessage, ElMessageBox, type CheckboxGroupValueType, type CheckboxValueType } from 'element-plus'
import {
  type CollectionOfWorks,
  type Works,
  type WokrsUserIds,
  type FileSystemOpenDirectoryOptions,
  worksIdStorageKey,
  useDirectoryPicker,
  toMap,
  arrayRemove,
} from 'shared'
import { request } from '@/utils/request'
import WorksAvatar from './WorksAvatar.vue'
import WorksDetails from './WorksDetails.vue'
import WorksDownload from './WorksDownload.vue'
import { VirtualList } from '../VirtualList'
import { RootDirectory, WorksDirectory, SortAmountDown, SortAmountUp } from '../Icons'
import { DownloadStatus, type WorksAwait } from './shared'

interface GlobalState {
  userId?: string
  checked: string[]
  checkAll: boolean
  indeterminate: boolean
  sort: SortType
  filter?: string
}

interface DownloadState {
  max: number
  isRunning: boolean
  awaitList: WorksAwait[]
  runningList: string[]
}

enum SortType {
  Down = 'down',
  Up = 'up',
}

export default defineComponent({
  name: 'Works',
  components: {
    WorksAvatar,
    WorksDetails,
    WorksDownload,
    VirtualList,
    Download,
    Folder,
    FolderOpened,
    Tools,
    RootDirectory,
    WorksDirectory,
    SortAmountDown,
    SortAmountUp,
  },
  setup() {
    const globalState = reactive<GlobalState>({
      checked: [],
      checkAll: false,
      indeterminate: false,
      sort: useStorage('sort', SortType.Up) as any,
    })

    const userIds = ref<WokrsUserIds>([])

    const worksState = reactive<{ [k: string]: CollectionOfWorks }>({})

    const worksUnWatch: Record<string, Unwatch> = {}

    storage.getItem<WokrsUserIds>(worksIdStorageKey, { fallback: [] }).then((ids) => {
      userIds.value = ids
      ids.forEach((id) => bindIdWorks(id))
    })

    storage.watch<WokrsUserIds>(worksIdStorageKey, (newValue, oldValue) => {
      newValue ??= []
      oldValue ??= []

      userIds.value = newValue

      newValue.forEach((newId) => {
        if (newId in worksUnWatch)
          for (let i = oldValue.length; i--; ) {
            if (oldValue[i] === newId) {
              oldValue.splice(i, 1)
              break
            }
          }
        else bindIdWorks(newId)
      })

      oldValue.forEach(unbindIdWorks)
    })

    function bindIdWorks(id: string) {
      storage.getItem<CollectionOfWorks>(`local:${id}`).then(
        (value) => {
          if (value) worksState[id] = value
          else delete worksState[id]
        },
        (error) => {}
      )

      worksUnWatch[id] = storage.watch<CollectionOfWorks>(`local:${id}`, (value) => {
        if (value) worksState[id] = value
        else delete worksState[id]
      })
    }

    const activeWorksList = computed(() => {
      const { userId, sort, filter } = globalState
      if (userId && userId in worksState) {
        let list = Object.values(worksState[userId].works)
        if (filter) {
          list = list.filter(({ title }) => title.includes(filter))
        }
        if (sort === SortType.Down) {
          list.reverse()
        }
        return list
      }
      return []
    })

    function unbindIdWorks(id: string) {
      worksUnWatch[id]?.()
      closeDirectory(worksState[id].name)
      delete worksUnWatch[id]
      delete worksState[id]
    }

    function onUserIdRemove(id: string) {
      if (globalState.userId === id) {
        globalState.userId = undefined
      }
      unbindIdWorks(id)
      const ids = userIds.value
      for (let i = ids.length; i--; ) {
        if (ids[i] === id) {
          ids.splice(i, 1)
          storage.setItem(worksIdStorageKey, toRaw(ids))
          return true
        }
      }
      return false
    }

    function onCheckAllChange(checkAll: CheckboxValueType) {
      checkAll = !!checkAll

      Object.assign<GlobalState, Partial<GlobalState>>(globalState, {
        checkAll,
        indeterminate: false,
        checked: checkAll ? activeWorksList.value.map(({ id }) => id) : [],
      })
    }

    function onUpdateChecked(value: CheckboxGroupValueType) {
      const checkAll = value.length === activeWorksList.value.length
      Object.assign<GlobalState, Partial<GlobalState>>(globalState, {
        checkAll,
        indeterminate: value.length ? !checkAll : false,
        checked: value as string[],
      })
    }

    const rootHandle = shallowRef<FileSystemDirectoryHandle | null>()

    const { showPicker, showRootPicker, writeToLocal, openDirectory, closeDirectory, getDirectoryKeys } =
      useDirectoryPicker({
        database: 'works',
        rootPickerOptions: { id: 'works_download', mode: 'readwrite', create: true },
        onRootHandlePicker(handle) {
          rootHandle.value = handle
        },
      })

    const worksHandle = shallowRef<FileSystemDirectoryHandle | null>()
    let worksDirectoryOptions: FileSystemOpenDirectoryOptions | undefined = undefined

    const downloadedList = computedAsync(async () => {
      const handle = worksHandle.value
      if (handle) {
        const list = await getDirectoryKeys(handle)

        return toMap(list, 'name')
      }
      return {}
    }, {})

    async function onAvatarClick(id: string) {
      Object.assign<GlobalState, Partial<GlobalState>>(globalState, {
        checkAll: false,
        indeterminate: false,
        checked: [],
        userId: id,
      })
      if (id in worksState) {
        worksDirectoryOptions = { id, mode: 'readwrite', create: true }
        if (rootHandle.value) {
          const handle = await openDirectory(worksState[id].name, worksDirectoryOptions)
          worksHandle.value = handle
        }
      } else {
        worksDirectoryOptions = undefined
        worksHandle.value = null
      }
    }

    function onRootDirectoryPick() {
      showRootPicker()
    }

    async function onWorksDirectoryPick() {
      if (!globalState.userId) return
      const handle = await showPicker(worksDirectoryOptions)
      worksHandle.value = handle
    }

    const downloadState = reactive<DownloadState>({
      max: useStorage('running-max', 1) as any,
      isRunning: false,
      awaitList: useStorage('await-list', []) as any,
      runningList: [],
    })

    function preventMessage(message: string) {
      ElMessageBox.confirm(message, '已为您省略以下视频（已存在于下载列表或已下载）', {
        dangerouslyUseHTMLString: true,
        showCancelButton: false,
        confirmButtonText: '确定',
      })
    }

    /**
     * 将被选中的添加到下载列表中
     */
    function onCheckedDownload() {
      const { userId, checked } = globalState
      if (!(userId! in worksState)) {
        return ElMessage.warning('还未选择作品集')
      }
      if (!checked.length) {
        return ElMessage.warning('请先选择需要下载的作品')
      }
      const { works, name: directoryName } = worksState[userId!]
      const downloaded = downloadedList.value
      const awaitDownload = downloadState.awaitList

      const omitList: string[] = []

      checked.forEach((worksId) => {
        const fileName = works[worksId].title
        if (!(fileName in downloaded) && !awaitDownload.find((item) => item.worksId === worksId)) {
          awaitDownload.push({
            userId: userId!,
            worksId: worksId,
            status: DownloadStatus.None,
            directory: directoryName,
            fileName: fileName,
          })
        } else {
          omitList.push(fileName)
        }
      })
      onCheckAllChange(false)
      onAwaitDownloadAll()

      omitList.length && preventMessage(omitList.join('<br/>'))
    }

    function onDownload(target: Works) {
      const { userId } = globalState
      const { name: directoryName } = worksState[userId!]
      const downloaded = downloadedList.value
      const awaitDownload = downloadState.awaitList
      const { title: fileName, id: worksId } = target

      if (!(fileName in downloaded) && !awaitDownload.find((item) => item.worksId === worksId)) {
        const worksAwait: WorksAwait = {
          userId: userId!,
          worksId: worksId,
          status: DownloadStatus.None,
          directory: directoryName,
          fileName: fileName,
        }
        awaitDownload.push(worksAwait)
        onAwaitDownload(worksAwait)
      } else {
        preventMessage(fileName)
      }
    }

    /**
     * 开始下载待下载队列中的所有
     */
    async function onAwaitDownloadAll() {
      downloadState.isRunning = true
      downloadState.awaitList.forEach((item) => {
        if (item.status !== DownloadStatus.Loading) {
          item.status = DownloadStatus.None
        }
      })
      executeAwaitNextDownload()
    }

    function onAwaitDownload(target: WorksAwait) {
      if (!downloadState.runningList.includes(target.worksId)) {
        executeAwaitDownload(target)
      }
    }

    /**
     * 寻找到下一个并执行下载
     */
    function executeAwaitNextDownload() {
      const { isRunning, max, runningList, awaitList } = downloadState
      if (isRunning && runningList.length < max && awaitList.length) {
        const nextItem = awaitList.find((item) => item.status === DownloadStatus.None)
        nextItem && executeAwaitDownload(nextItem)
        executeAwaitNextDownload()
      }
    }

    const worksAwaitAbort = new WeakMap<WorksAwait, (reason?: any) => void>()

    function executeAbort(target: WorksAwait, reason?: any) {
      const abort = worksAwaitAbort.get(target)
      if (abort) {
        worksAwaitAbort.delete(target)
        abort(reason)
      }
    }

    /**
     * 执行下载
     */
    async function executeAwaitDownload(target: WorksAwait) {
      if (!worksHandle.value) {
        await onWorksDirectoryPick()
      }
      target.status = DownloadStatus.Loading
      downloadState.runningList.push(target.worksId)
      const { userId, worksId } = target
      const url = worksState[userId]?.works[worksId]?.video.at(-1)
      if (url) {
        try {
          const controller = new AbortController()
          worksAwaitAbort.set(target, (reason) => controller.abort())
          const { data } = await request.get<Blob>(url, {
            signal: controller.signal,
            onDownloadProgress({ progress }) {
              target.percent = progress ? (progress * 100).toFixed(2) : ''
            },
          })
          await writeToLocal({
            blob: data,
            directory: target.directory,
            name: target.fileName,
            directoryOptions: { id: target.directory, mode: 'readwrite' },
          })
          arrayRemove(downloadState.awaitList, (item) => item.worksId === target.worksId, true)
        } catch (error) {
          console.error(error)
          target.status = DownloadStatus.Fail
        } finally {
          worksAwaitAbort.delete(target)
          const { runningList } = downloadState
          arrayRemove(runningList, (id) => id === target.worksId, true)

          executeAwaitNextDownload()
        }
      }
    }

    /**
     * 移除下载
     * @param target 被取消的项。不传时移除全部。
     */
    function onAwaitDownloadRemove(target?: WorksAwait) {
      const { runningList, awaitList } = downloadState
      if (target) {
        arrayRemove(runningList, (worksId) => worksId === target.worksId, true)
        arrayRemove(awaitList, (item) => item.worksId === target.worksId, true)
        executeAbort(target, `将 ${target.fileName} 从下载列表中移除`)
      } else {
        for (let i = 0; i < awaitList.length; i++) {
          if (!runningList.length) break

          const item = awaitList[i]
          const index = runningList.indexOf(item.worksId)
          if (~index) {
            runningList.splice(index, 1)
            executeAbort(item, `将 ${item.fileName} 从下载列表中移除`)
          }
        }

        awaitList.length = 0
      }
    }

    /**
     * 取消下载
     * @param target 被取消的项。不传时取消全部。
     */
    function onAwaitDownloadStop(target?: WorksAwait) {
      const worksId = target?.worksId
      if (!worksId) downloadState.isRunning = false
      const list: WorksAwait[] = []

      const { runningList, awaitList } = downloadState
      if (worksId) {
        arrayRemove(runningList, (id) => id == worksId)
        const item = awaitList.find((item) => item.worksId === worksId)
        if (item) list.push(item)
      } else {
        for (let i = 0; i < awaitList.length; i++) {
          if (!runningList.length) break

          const item = awaitList[i]
          const index = runningList.indexOf(item.worksId)
          if (~index) {
            runningList.splice(index, 1)
            list.push(item)
          }
        }
      }

      list.forEach((item) => {
        item.percent = ''
        item.status = DownloadStatus.None
        executeAbort(item, `暂停 ${item.fileName} 下载`)
      })
    }

    return {
      SortType,
      globalState,
      userIds,
      worksState,
      activeWorksList,
      onUserIdRemove,
      onCheckAllChange,
      onUpdateChecked,
      rootHandle,
      worksHandle,
      downloadedList,
      onAvatarClick,
      onRootDirectoryPick,
      onWorksDirectoryPick,
      downloadState,
      onCheckedDownload,
      onDownload,
      onAwaitDownload,
      onAwaitDownloadAll,
      onAwaitDownloadRemove,
      onAwaitDownloadStop,
    }
  },
})
</script>

<style>
.cursor-pointer {
  cursor: pointer;
}
.works-directory-button {
  width: 100%;
  justify-content: start;
}
.align-right {
  margin-left: auto;
}
</style>

<style lang="scss">
.works-container {
  display: grid;
  grid-template-columns: minmax(200px, max-content) 1fr 300px;
  width: 100vw;
  height: 100vh;

  .works-aside {
    height: 100%;
    overflow-y: auto;
  }

  .works-content {
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-left: 1px solid #999;
    border-right: 1px solid #999;
  }

  .works-content__header {
    flex: 0;
    border-bottom: 1px solid #999;
    padding: 20px;
    z-index: 1;
    background-color: #fff;
    display: flex;
    column-gap: 20px;
    align-items: center;

    .header-item {
      display: flex;
      align-items: center;
      cursor: pointer;
    }
  }

  .works-content__body {
    flex: 1;
    overflow: hidden;
    margin: 20px 0;
  }

  .works-card-container {
    position: relative;
  }

  .works-card-tool {
    position: absolute;
    top: 0;
    z-index: 0;
    display: flex;
    align-items: center;
    column-gap: 10px;
    width: 100%;
    padding: 0 20px;
    box-sizing: border-box;
    background: #9999991a;

    & > * {
      cursor: pointer;
    }
  }
}
</style>
